package com.meshnetics.gb.type.impl.zcl;

import com.meshnetics.gb.type.DataTypeID;
import com.meshnetics.gb.type.DataValue;
import com.meshnetics.gb.type.MandatoryFormatter;
import com.meshnetics.gb.type.impl.ByteUtils;
import com.meshnetics.gb.type.impl.StringUtils;

import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.Arrays;

/**
 * WRITE DESCRIPTION HERE
 *
 * @author AKovylin
 */
public abstract class FixedBytesDataType extends AbstractDataType {
    protected static final int BITS_IN_BYTE = 8;

    private MandatoryFormatter formatter = new FixedBytesMandatoryFormatter(this);
    protected final int byteSize;

    public FixedBytesDataType(String[] aliases, int byteSize) {
        this(aliases, null, byteSize);
    }

    public FixedBytesDataType(String[] aliases, DataTypeID dataTypeId, int byteSize) {
        super(aliases, dataTypeId);
        if (byteSize < 1) {
            throw new IllegalArgumentException("invalid byteSize " + byteSize);
        }
        this.byteSize = byteSize;
    }

    public int getByteSize() {
        return byteSize;
    }

    public int getBitSize() {
        return byteSize * BITS_IN_BYTE;
    }

    public DataValue fromObject(Object value) {
        if (!(value instanceof Number)) {
            throw new IllegalArgumentException("not a Number");
        }
        BigInteger bi = value instanceof BigInteger ? (BigInteger) value : null;
        if (bi == null && byteSize <= 8) { // we can use longValue() and treat it as array of bytes
            long l = ((Number) value).longValue();
            // we take more than ZCL allow - cause invalid value is allowed as data value (only marked as invalid)
            // we accept -(2^(n-1)) - 2^n-1 range to take all possible signed/unsigned combinations
            if (l < -Math.pow(2, byteSize * BITS_IN_BYTE) ||
                    l > (Math.pow(2, byteSize * BITS_IN_BYTE) - 1)) {
                throw new IllegalArgumentException("out of range");
            }
            byte[] bytes = new byte[byteSize];
            ByteUtils.fillFromLong(l, bytes, 0, byteSize);
            return createValue(bytes);
        } else {
            if (bi == null) { // BigInteger was required but not found
                throw new IllegalArgumentException("not a BigInteger");
            } else if (bi.bitLength() > byteSize * BITS_IN_BYTE) { // out of range
                throw new IllegalArgumentException("out of range");
            }
            byte[] val = new byte[byteSize];
            byte[] biVal = bi.toByteArray();
            // byte array from BigInteger can include additional sign bit
            // (0 for positive as 25th bit if 24 bits value big enough to have
            // 0th (leftmost) bit=1 (and its not sign bit))
            if (biVal.length > val.length) {
                System.arraycopy(biVal, biVal.length - val.length, val, 0, val.length);
            } else {
                System.arraycopy(biVal, 0, val, val.length - biVal.length, biVal.length);
                Arrays.fill(val, 0, val.length - biVal.length, (bi.signum() > 0 ? (byte) 0x00 : (byte) 0xff));
            }
            return createValue(val);
        }
    }

    protected void setFormatter(MandatoryFormatter formatter) {
        if (formatter == null) {
            throw new NullPointerException("formatter is null");
        }
        this.formatter = formatter;
    }

    public DataValue valueOf(byte[] value) {
        if (value == null) {
            throw new NullPointerException("value is null");
        }
        if (value.length != byteSize) {
            throw new IllegalArgumentException("value must contain exactly " + byteSize + " bytes");
        }
        return createValue((byte[]) value.clone());
    }

    public DataValue deserialize(ByteBuffer bb) {
        ByteUtils.ensureNotNull(bb);
        ByteUtils.ensureCapacity(bb, byteSize);
        byte[] value = new byte[byteSize];
        ByteUtils.readFromByteBuffer(bb, value);
        return createValue(value);
    }

    // no byte array cloning needed
    protected abstract FixedBytesDataValue createValue(byte[] bytes);

    public MandatoryFormatter getMandatoryFormatter() {
        return formatter;
    }

    protected class FixedBytesMandatoryFormatter extends AbstractMandatoryFormatter {
        public FixedBytesMandatoryFormatter(FixedBytesDataType valueType) {
            super(valueType);
        }

        public DataValue valueOf(String serializedValue) {
            if (serializedValue == null) {
                throw new NullPointerException("serializedValue is null");
            }
            // 2 hex chars per byte
            if (serializedValue.length() != FixedBytesDataType.this.byteSize * 2) {
                throw new IllegalArgumentException("serializedValue must contain exactly " +
                        FixedBytesDataType.this.byteSize + " characters");
            }
            return createValue(StringUtils.recoverBytesFromHexString(serializedValue, 0, serializedValue.length()));
        }

        public String formatSerializedString(DataValue value) {
            checkValue(value);
            return StringUtils.toHexString(((FixedBytesDataValue) value).value);
        }
    }
}
